{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 评估并优化生成部分"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在前面的章节中，我们讲到了如何评估一个基于 RAG 框架的大模型应用的整体性能。通过针对性构造验证集，可以采用多种方法从多个维度对系统性能进行评估。但是，评估的目的是为了更好地优化应用效果，要优化应用性能，我们需要结合评估结果，对评估出的 Bad Case 进行拆分，并分别对每一部分做出评估和优化。\n",
    "\n",
    "RAG 全称为检索增强生成，因此，其有两个核心部分：检索部分和生成部分。检索部分的核心功能是保证系统根据用户 query 能够查找到对应的答案片段，而生成部分的核心功能即是保证系统在获得了正确的答案片段之后，可以充分发挥大模型能力生成一个满足用户要求的正确回答。\n",
    "\n",
    "优化一个大模型应用，我们往往需要从这两部分同时入手，分别评估检索部分和优化部分的性能，找出 Bad Case 并针对性进行性能的优化。而具体到生成部分，在已限定使用的大模型基座的情况下，我们往往会通过优化 Prompt Engineering 来优化生成的回答。在本章中，我们将首先结合我们刚刚搭建出的大模型应用实例——个人知识库助手，向大家讲解如何评估分析生成部分性能，针对性找出 Bad Case，并通过优化 Prompt Engineering 的方式来优化生成部分。\n",
    "\n",
    "在正式开始之前，我们先加载我们的向量数据库与检索链："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys\n",
    "sys.path.append(\"../C3 搭建知识库\") # 将父目录放入系统路径中\n",
    "\n",
    "# 使用智谱 Embedding API，注意，需要将上一章实现的封装代码下载到本地\n",
    "from zhipuai_embedding import ZhipuAIEmbeddings\n",
    "\n",
    "from langchain.vectorstores.chroma import Chroma\n",
    "from langchain_openai import ChatOpenAI\n",
    "from dotenv import load_dotenv, find_dotenv\n",
    "import os\n",
    "\n",
    "_ = load_dotenv(find_dotenv())    # read local .env file\n",
    "zhipuai_api_key = os.environ['ZHIPUAI_API_KEY']\n",
    "OPENAI_API_KEY = os.environ[\"OPENAI_API_KEY\"]\n",
    "\n",
    "# 定义 Embeddings\n",
    "embedding = ZhipuAIEmbeddings()\n",
    "\n",
    "# 向量数据库持久化路径\n",
    "persist_directory = '../../data_base/vector_db/chroma'\n",
    "\n",
    "# 加载数据库\n",
    "vectordb = Chroma(\n",
    "    persist_directory=persist_directory,  # 允许我们将persist_directory目录保存到磁盘上\n",
    "    embedding_function=embedding\n",
    ")\n",
    "\n",
    "# 使用 OpenAI GPT-3.5 模型\n",
    "llm = ChatOpenAI(model_name = \"gpt-3.5-turbo\", temperature = 0)\n",
    "\n",
    "os.environ['HTTPS_PROXY'] = 'http://127.0.0.1:7890'\n",
    "os.environ[\"HTTP_PROXY\"] = 'http://127.0.0.1:7890'\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们先使用初始化的 Prompt 创建一个基于模板的检索链："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.prompts import PromptTemplate\n",
    "from langchain.chains import RetrievalQA\n",
    "\n",
    "\n",
    "template_v1 = \"\"\"使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答\n",
    "案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问！”。\n",
    "{context}\n",
    "问题: {question}\n",
    "\"\"\"\n",
    "\n",
    "QA_CHAIN_PROMPT = PromptTemplate(input_variables=[\"context\",\"question\"],\n",
    "                                 template=template_v1)\n",
    "\n",
    "\n",
    "\n",
    "qa_chain = RetrievalQA.from_chain_type(llm,\n",
    "                                       retriever=vectordb.as_retriever(),\n",
    "                                       return_source_documents=True,\n",
    "                                       chain_type_kwargs={\"prompt\":QA_CHAIN_PROMPT})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "先测试一下效果："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "南瓜书是对《机器学习》（西瓜书）中比较难理解的公式进行解析和补充推导细节的书籍。南瓜书的最佳使用方法是以西瓜书为主线，遇到推导困难或看不懂的公式时再来查阅南瓜书。谢谢你的提问！\n"
     ]
    }
   ],
   "source": [
    "question = \"什么是南瓜书\"\n",
    "result = qa_chain({\"query\": question})\n",
    "print(result[\"result\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. 提升直观回答质量"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "寻找 Bad Case 的思路有很多，最直观也最简单的就是评估直观回答的质量，结合原有资料内容，判断在什么方面有所不足。例如，上述的测试我们可以构造成一个 Bad Case：\n",
    "\n",
    "    问题：什么是南瓜书\n",
    "    初始回答：南瓜书是对《机器学习》（西瓜书）中难以理解的公式进行解析和补充推导细节的一本书。谢谢你的提问！\n",
    "    存在不足：回答太简略，需要回答更具体；谢谢你的提问感觉比较死板，可以去掉\n",
    "我们再针对性地修改 Prompt 模板，加入要求其回答具体，并去掉“谢谢你的提问”的部分："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "南瓜书是一本针对周志华老师的《机器学习》（西瓜书）的补充解析书籍。它旨在对西瓜书中比较难理解的公式进行解析，并补充具体的推导细节，以帮助读者更好地理解机器学习领域的知识。南瓜书的内容是以西瓜书为前置知识进行表述的，最佳使用方法是在遇到自己推导不出来或者看不懂的公式时来查阅。南瓜书的编写团队致力于帮助读者成为合格的“理工科数学基础扎实点的大二下学生”，并提供了在线阅读地址和最新版PDF获取地址供读者使用。\n"
     ]
    }
   ],
   "source": [
    "template_v2 = \"\"\"使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答\n",
    "案。你应该使答案尽可能详细具体，但不要偏题。如果答案比较长，请酌情进行分段，以提高答案的阅读体验。\n",
    "{context}\n",
    "问题: {question}\n",
    "有用的回答:\"\"\"\n",
    "\n",
    "QA_CHAIN_PROMPT = PromptTemplate(input_variables=[\"context\",\"question\"],\n",
    "                                 template=template_v2)\n",
    "qa_chain = RetrievalQA.from_chain_type(llm,\n",
    "                                       retriever=vectordb.as_retriever(),\n",
    "                                       return_source_documents=True,\n",
    "                                       chain_type_kwargs={\"prompt\":QA_CHAIN_PROMPT})\n",
    "\n",
    "question = \"什么是南瓜书\"\n",
    "result = qa_chain({\"query\": question})\n",
    "print(result[\"result\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，改进后的 v2 版本能够给出更具体、详细的回答，解决了之前的问题。但是我们可以进一步思考，要求模型给出具体、详细的回答，是否会导致针对一些有要点的回答没有重点、模糊不清？我们测试以下问题："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "在使用大型语言模型时，构造Prompt的原则主要包括编写清晰、具体的指令和给予模型充足的思考时间。首先，Prompt需要清晰明确地表达需求，提供足够的上下文信息，以确保语言模型准确理解用户的意图。这就好比向一个对人类世界一无所知的外星人解释事物一样，需要详细而清晰的描述。过于简略的Prompt会导致模型难以准确把握任务要求。\n",
      "\n",
      "其次，给予语言模型充足的推理时间也是至关重要的。类似于人类解决问题时需要思考的时间，模型也需要时间来推理和生成准确的结果。匆忙的结论往往会导致错误的输出。因此，在设计Prompt时，应该加入逐步推理的要求，让模型有足够的时间进行逻辑思考，从而提高结果的准确性和可靠性。\n",
      "\n",
      "通过遵循这两个原则，设计优化的Prompt可以帮助语言模型充分发挥潜力，完成复杂的推理和生成任务。掌握这些Prompt设计原则是开发者成功应用语言模型的重要一步。在实际应用中，不断优化和调整Prompt，逐步逼近最佳形式，是构建高效、可靠模型交互的关键策略。\n"
     ]
    }
   ],
   "source": [
    "question = \"使用大模型时，构造 Prompt 的原则有哪些\"\n",
    "result = qa_chain({\"query\": question})\n",
    "print(result[\"result\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，针对我们关于 LLM 课程的提问，模型回答确实详细具体，也充分参考了课程内容，但回答使用首先、其次等词开头，同时将整体答案分成了4段，导致答案不是特别重点清晰，不容易阅读。因此，我们构造以下 Bad Case：\n",
    "\n",
    "    问题：使用大模型时，构造 Prompt 的原则有哪些\n",
    "    初始回答：略\n",
    "    存在不足：没有重点，模糊不清\n",
    "\n",
    "针对该 Bad Case，我们可以改进 Prompt，要求其对有几点的答案进行分点标号，让答案清晰具体：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1. 编写清晰、具体的指令是构造 Prompt 的第一原则。Prompt需要明确表达需求，提供充足上下文，使语言模型准确理解意图。过于简略的Prompt会使模型难以完成任务。\n",
      "\n",
      "2. 给予模型充足思考时间是构造Prompt的第二原则。语言模型需要时间推理和解决复杂问题，匆忙得出的结论可能不准确。因此，Prompt应该包含逐步推理的要求，让模型有足够时间思考，生成更准确的结果。\n",
      "\n",
      "3. 在设计Prompt时，要指定完成任务所需的步骤。通过给定一个复杂任务，给出完成任务的一系列步骤，可以帮助模型更好地理解任务要求，提高任务完成的效率。\n",
      "\n",
      "4. 迭代优化是构造Prompt的常用策略。通过不断尝试、分析结果、改进Prompt的过程，逐步逼近最优的Prompt形式。成功的Prompt通常是通过多轮调整得出的。\n",
      "\n",
      "5. 添加表格描述是优化Prompt的一种方法。要求模型抽取信息并组织成表格，指定表格的列、表名和格式，可以帮助模型更好地理解任务，并生成符合预期的结果。\n",
      "\n",
      "总之，构造Prompt的原则包括清晰具体的指令、给予模型充足思考时间、指定完成任务所需的步骤、迭代优化和添加表格描述等。这些原则可以帮助开发者设计出高效、可靠的Prompt，发挥语言模型的最大潜力。\n"
     ]
    }
   ],
   "source": [
    "template_v3 = \"\"\"使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答\n",
    "案。你应该使答案尽可能详细具体，但不要偏题。如果答案比较长，请酌情进行分段，以提高答案的阅读体验。\n",
    "如果答案有几点，你应该分点标号回答，让答案清晰具体\n",
    "{context}\n",
    "问题: {question}\n",
    "有用的回答:\"\"\"\n",
    "\n",
    "QA_CHAIN_PROMPT = PromptTemplate(input_variables=[\"context\",\"question\"],\n",
    "                                 template=template_v3)\n",
    "qa_chain = RetrievalQA.from_chain_type(llm,\n",
    "                                       retriever=vectordb.as_retriever(),\n",
    "                                       return_source_documents=True,\n",
    "                                       chain_type_kwargs={\"prompt\":QA_CHAIN_PROMPT})\n",
    "\n",
    "question = \"使用大模型时，构造 Prompt 的原则有哪些\"\n",
    "result = qa_chain({\"query\": question})\n",
    "print(result[\"result\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "提升回答质量的方法还有很多，核心是围绕具体业务展开思考，找出初始回答中不足以让人满意的点，并针对性进行提升改进，此处不再赘述。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. 标明知识来源，提高可信度"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "由于大模型存在幻觉问题，有时我们会怀疑模型回答并非源于已有知识库内容，这对一些需要保证真实性的场景来说尤为重要，例如："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "强化学习是一种机器学习方法，旨在让智能体通过与环境的交互学习如何做出一系列好的决策。在强化学习中，智能体会根据环境的状态选择一个动作，然后根据环境的反馈（奖励）来调整其策略，以最大化长期奖励。强化学习的目标是在不确定的情况下做出最优的决策，类似于让一个小孩通过不断尝试来学会走路的过程。强化学习的应用范围广泛，包括游戏玩法、机器人控制、交通优化等领域。在强化学习中，智能体和环境之间不断交互，智能体根据环境的反馈来调整其策略，以获得最大的奖励。\n"
     ]
    }
   ],
   "source": [
    "question = \"强化学习的定义是什么\"\n",
    "result = qa_chain({\"query\": question})\n",
    "print(result[\"result\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们可以要求模型在生成回答时注明知识来源，这样可以避免模型杜撰并不存在于给定资料的知识，同时，也可以提高我们对模型生成答案的可信度："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "强化学习是一种机器学习方法，旨在让智能体通过与环境的交互学习如何做出一系列好的决策。在这个过程中，智能体会根据环境的反馈（奖励）来调整自己的行为，以最大化长期奖励的总和。强化学习的目标是在不确定的情况下做出最优的决策，类似于让一个小孩通过不断尝试来学会走路的过程。强化学习的交互过程由智能体和环境两部分组成，智能体根据环境的状态选择动作，环境根据智能体的动作输出下一个状态和奖励。强化学习的应用非常广泛，包括游戏玩法、机器人控制、交通管理等领域。【来源：蘑菇书一语二语二强化学习教程】。\n"
     ]
    }
   ],
   "source": [
    "template_v4 = \"\"\"使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答\n",
    "案。你应该使答案尽可能详细具体，但不要偏题。如果答案比较长，请酌情进行分段，以提高答案的阅读体验。\n",
    "如果答案有几点，你应该分点标号回答，让答案清晰具体。\n",
    "请你附上回答的来源原文，以保证回答的正确性。\n",
    "{context}\n",
    "问题: {question}\n",
    "有用的回答:\"\"\"\n",
    "\n",
    "QA_CHAIN_PROMPT = PromptTemplate(input_variables=[\"context\",\"question\"],\n",
    "                                 template=template_v4)\n",
    "qa_chain = RetrievalQA.from_chain_type(llm,\n",
    "                                       retriever=vectordb.as_retriever(),\n",
    "                                       return_source_documents=True,\n",
    "                                       chain_type_kwargs={\"prompt\":QA_CHAIN_PROMPT})\n",
    "\n",
    "question = \"强化学习的定义是什么\"\n",
    "result = qa_chain({\"query\": question})\n",
    "print(result[\"result\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "但是，附上原文来源往往会导致上下文的增加以及回复速度的降低，我们需要根据业务场景酌情考虑是否要求附上原文。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. 构造思维链"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "大模型往往可以很好地理解并执行指令，但模型本身还存在一些能力的限制，例如大模型的幻觉、无法理解较为复杂的指令、无法执行复杂步骤等。我们可以通过构造思维链，将 Prompt 构造成一系列步骤来尽量减少其能力限制，例如，我们可以构造一个两步的思维链，要求模型在第二步做出反思，以尽可能消除大模型的幻觉问题。\n",
    "\n",
    "我们首先有这样一个 Bad Case：\n",
    "\n",
    "    问题：我们应该如何去构造一个 LLM 项目\n",
    "    初始回答：略\n",
    "    存在不足：事实上，知识库中中关于如何构造LLM项目的内容是使用 LLM API 去搭建一个应用，模型的回答看似有道理，实则是大模型的幻觉，将部分相关的文本拼接得到，存在问题"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "构建一个LLM项目需要考虑以下几个步骤：\n",
      "\n",
      "1. 确定项目目标和需求：首先要明确你的项目是为了解决什么问题或实现什么目标，确定需要使用LLM的具体场景和任务。\n",
      "\n",
      "2. 收集和准备数据：根据项目需求，收集和准备适合的数据集，确保数据的质量和多样性，以提高LLM的性能和效果。\n",
      "\n",
      "3. 设计Prompt和指令微调：根据项目需求设计合适的Prompt，确保指令清晰明确，可以引导LLM生成符合预期的文本。\n",
      "\n",
      "4. 进行模型训练和微调：使用基础LLM或指令微调LLM对数据进行训练和微调，以提高模型在特定任务上的表现和准确性。\n",
      "\n",
      "5. 测试和评估模型：在训练完成后，对模型进行测试和评估，检查其在不同场景下的表现和效果，根据评估结果进行必要的调整和优化。\n",
      "\n",
      "6. 部署和应用模型：将训练好的LLM模型部署到实际应用中，确保其能够正常运行并实现预期的效果，持续监测和优化模型的性能。\n",
      "\n",
      "来源：根据提供的上下文内容进行总结。\n"
     ]
    }
   ],
   "source": [
    "question = \"我们应该如何去构造一个LLM项目\"\n",
    "result = qa_chain({\"query\": question})\n",
    "print(result[\"result\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对此，我们可以优化 Prompt，将之前的 Prompt 变成两个步骤，要求模型在第二个步骤中做出反思："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "根据上下文中提供的信息，构造一个LLM项目需要考虑以下几个步骤：\n",
      "\n",
      "1. 确定项目目标：首先要明确你的项目目标是什么，是要进行文本摘要、情感分析、实体提取还是其他任务。根据项目目标来确定LLM的使用方式和调用API接口的方法。\n",
      "\n",
      "2. 设计Prompt：根据项目目标设计合适的Prompt，Prompt应该清晰明确，指导LLM生成符合预期的结果。Prompt的设计需要考虑到任务的具体要求，比如在文本摘要任务中，Prompt应该包含需要概括的文本内容。\n",
      "\n",
      "3. 调用API接口：根据设计好的Prompt，通过编程调用LLM的API接口来生成结果。确保API接口的调用方式正确，以获取准确的结果。\n",
      "\n",
      "4. 分析结果：获取LLM生成的结果后，进行结果分析，确保结果符合项目目标和预期。如果结果不符合预期，可以调整Prompt或者其他参数再次生成结果。\n",
      "\n",
      "5. 优化和改进：根据分析结果的反馈，不断优化和改进LLM项目，提高项目的效率和准确性。可以尝试不同的Prompt设计、调整API接口的参数等方式来优化项目。\n",
      "\n",
      "通过以上步骤，可以构建一个有效的LLM项目，利用LLM的强大功能来实现文本摘要、情感分析、实体提取等任务，提高工作效率和准确性。如果有任何不清楚的地方或需要进一步的指导，可以随时向相关领域的专家寻求帮助。\n"
     ]
    }
   ],
   "source": [
    "template_v4 = \"\"\"\n",
    "请你依次执行以下步骤：\n",
    "① 使用以下上下文来回答最后的问题。如果你不知道答案，就说你不知道，不要试图编造答案。\n",
    "你应该使答案尽可能详细具体，但不要偏题。如果答案比较长，请酌情进行分段，以提高答案的阅读体验。\n",
    "如果答案有几点，你应该分点标号回答，让答案清晰具体。\n",
    "上下文：\n",
    "{context}\n",
    "问题: \n",
    "{question}\n",
    "有用的回答:\n",
    "② 基于提供的上下文，反思回答中有没有不正确或不是基于上下文得到的内容，如果有，回答你不知道\n",
    "确保你执行了每一个步骤，不要跳过任意一个步骤。\n",
    "\"\"\"\n",
    "\n",
    "QA_CHAIN_PROMPT = PromptTemplate(input_variables=[\"context\",\"question\"],\n",
    "                                 template=template_v4)\n",
    "qa_chain = RetrievalQA.from_chain_type(llm,\n",
    "                                       retriever=vectordb.as_retriever(),\n",
    "                                       return_source_documents=True,\n",
    "                                       chain_type_kwargs={\"prompt\":QA_CHAIN_PROMPT})\n",
    "\n",
    "question = \"我们应该如何去构造一个LLM项目\"\n",
    "result = qa_chain({\"query\": question})\n",
    "print(result[\"result\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看出，要求模型做出自我反思之后，模型修复了自己的幻觉，给出了正确的答案。我们还可以通过构造思维链完成更多功能，此处就不再赘述了，欢迎读者尝试。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. 增加一个指令解析"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们往往会面临一个需求，即我们需要模型以我们指定的格式进行输出。但是，由于我们使用了 Prompt Template 来填充用户问题，用户问题中存在的格式要求往往会被忽略，例如："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "根据上下文提供的信息，LLM（Large Language Model）的分类可以分为两种类型，即基础LLM和指令微调LLM。基础LLM是基于文本训练数据，训练出预测下一个单词能力的模型，通常通过在大量数据上训练来确定最可能的词。指令微调LLM则是对基础LLM进行微调，以更好地适应特定任务或场景，类似于向另一个人提供指令来完成任务。\n",
      "\n",
      "根据上下文，可以返回一个Python List，其中包含LLM的两种分类：[\"基础LLM\", \"指令微调LLM\"]。\n"
     ]
    }
   ],
   "source": [
    "question = \"LLM的分类是什么？给我返回一个 Python List\"\n",
    "result = qa_chain({\"query\": question})\n",
    "print(result[\"result\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，虽然我们要求模型给返回一个 Python List，但该输出要求被包裹在 Template 中被模型忽略掉了。针对该问题，我们可以构造一个 Bad Case：\n",
    "\n",
    "    问题：LLM的分类是什么？给我返回一个 Python List\n",
    "    初始回答：根据提供的上下文，LLM的分类可以分为基础LLM和指令微调LLM。\n",
    "    存在不足：没有按照指令中的要求输出\n",
    "\n",
    "针对该问题，一个存在的解决方案是，在我们的检索 LLM 之前，增加一层 LLM 来实现指令的解析，将用户问题的格式要求和问题内容拆分开来。这样的思路其实就是目前大火的 Agent 机制的雏形，即针对用户指令，设置一个 LLM（即 Agent）来理解指令，判断指令需要执行什么工具，再针对性调用需要执行的工具，其中每一个工具可以是基于不同 Prompt Engineering 的 LLM，也可以是例如数据库、API 等。LangChain 中其实有设计 Agent 机制，但本教程中我们就不再赘述了，这里只基于 OpenAI 的原生接口简单实现这一功能："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 使用第二章讲过的 OpenAI 原生接口\n",
    "\n",
    "from openai import OpenAI\n",
    "\n",
    "client = OpenAI(\n",
    "    # This is the default and can be omitted\n",
    "    api_key=os.environ.get(\"OPENAI_API_KEY\"),\n",
    ")\n",
    "\n",
    "\n",
    "def gen_gpt_messages(prompt):\n",
    "    '''\n",
    "    构造 GPT 模型请求参数 messages\n",
    "    \n",
    "    请求参数：\n",
    "        prompt: 对应的用户提示词\n",
    "    '''\n",
    "    messages = [{\"role\": \"user\", \"content\": prompt}]\n",
    "    return messages\n",
    "\n",
    "\n",
    "def get_completion(prompt, model=\"gpt-3.5-turbo\", temperature = 0):\n",
    "    '''\n",
    "    获取 GPT 模型调用结果\n",
    "\n",
    "    请求参数：\n",
    "        prompt: 对应的提示词\n",
    "        model: 调用的模型，默认为 gpt-3.5-turbo，也可以按需选择 gpt-4 等其他模型\n",
    "        temperature: 模型输出的温度系数，控制输出的随机程度，取值范围是 0~2。温度系数越低，输出内容越一致。\n",
    "    '''\n",
    "    response = client.chat.completions.create(\n",
    "        model=model,\n",
    "        messages=gen_gpt_messages(prompt),\n",
    "        temperature=temperature,\n",
    "    )\n",
    "    if len(response.choices) > 0:\n",
    "        return response.choices[0].message.content\n",
    "    return \"generate answer error\"\n",
    "\n",
    "prompt_input = '''\n",
    "请判断以下问题中是否包含对输出的格式要求，并按以下要求输出：\n",
    "请返回给我一个可解析的Python列表，列表第一个元素是对输出的格式要求，应该是一个指令；第二个元素是去掉格式要求的问题原文\n",
    "如果没有格式要求，请将第一个元素置为空\n",
    "需要判断的问题：\n",
    "```\n",
    "{}\n",
    "```\n",
    "不要输出任何其他内容或格式，确保返回结果可解析。\n",
    "'''\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们测试一下该 LLM 分解格式要求的能力：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'```\\n[\"给我返回一个 Python List\", \"LLM的分类是什么？\"]\\n```'"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = get_completion(prompt_input.format(question))\n",
    "response"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，通过上述 Prompt，LLM 可以很好地实现输出格式的解析，接下来，我们可以再设置一个 LLM 根据输出格式要求，对输出内容进行解析："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "prompt_output = '''\n",
    "请根据回答文本和输出格式要求，按照给定的格式要求对问题做出回答\n",
    "需要回答的问题：\n",
    "```\n",
    "{}\n",
    "```\n",
    "回答文本：\n",
    "```\n",
    "{}\n",
    "```\n",
    "输出格式要求：\n",
    "```\n",
    "{}\n",
    "```\n",
    "'''"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然后我们可以将两个 LLM 与检索链串联起来：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\"['基础LLM', '指令微调LLM']\""
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "question = 'LLM的分类是什么？给我返回一个 Python List'\n",
    "# 首先将格式要求与问题拆分\n",
    "input_lst_s = get_completion(prompt_input.format(question))\n",
    "# 找到拆分之后列表的起始和结束字符\n",
    "start_loc = input_lst_s.find('[')\n",
    "end_loc = input_lst_s.find(']')\n",
    "rule, new_question = eval(input_lst_s[start_loc:end_loc+1])\n",
    "# 接着使用拆分后的问题调用检索链\n",
    "result = qa_chain({\"query\": new_question})\n",
    "result_context = result[\"result\"]\n",
    "# 接着调用输出格式解析\n",
    "response = get_completion(prompt_output.format(new_question, result_context, rule))\n",
    "response"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，经过如上步骤，我们就成功地实现了输出格式的限定。当然，在上面代码中，核心为介绍 Agent 思想，事实上，不管是 Agent 机制还是 Parser 机制（也就是限定输出格式），LangChain 都提供了成熟的工具链供使用，欢迎感兴趣的读者深入探讨，此处就不展开讲解了。\n",
    "\n",
    "通过上述讲解的思路，结合实际业务情况，我们可以不断发现 Bad Case 并针对性优化 Prompt，从而提升生成部分的性能。但是，上述优化的前提是检索部分能够检索到正确的答案片段，也就是检索的准确率和召回率尽可能高。那么，如何能够评估并优化检索部分的性能呢？下一章我们会深入探讨这个问题。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "llm_universe",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
